perf: incremental memory optimization across Maka - #5153
Conversation
Create the WorkHub renderer on first use rather than on enable, while retaining drafts after use. Load platform SDKs only when a configured bot starts or Slack credentials are tested. Generated-by: OpenAI Codex
Track Blob URL ownership before decode and stop asynchronous staging after unmount while preserving hidden drafts and StrictMode replay. Generated-by: OpenAI Codex
Retain canonical content digests instead of historical submit and queue edit bodies, without evicting epoch-long outcomes or changing pending operations. Generated-by: OpenAI Codex
Cancel the losing animation frame or timeout after a stream display batch flush, preventing suspended background frames from retaining completed handlers. Generated-by: OpenAI Codex
Fence old supervisor callbacks and preserve explicit reconcile readers across epoch handover. Retain lightweight driver metadata for historical projection repair and close diagnostics. Generated-by: OpenAI Codex
Replace connection-lifetime Promise races with removable abort listeners so released transcript overlay buffers can be collected while the connection remains open. Generated-by: OpenAI Codex
Drop per-session release epochs when connection attempts settle and avoid retaining tombstones for sessions that never acquired a browser, preserving reentrant teardown and single-flight fences. Generated-by: OpenAI Codex
Retain canonical content digests and owned identity metadata for historical admissions while keeping full current tips and validated recovery or conflict results. Generated-by: OpenAI Codex
Release the SQLite lease before removing temporary files so root admission tests clean up on Windows and do not retain fixture database owners. Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Keep frame cleanup within the renderer architecture boundary without widening legacy budgets. Generated-by: OpenAI Codex
Keep cycle memoization scoped to each parse and restore allocation state when parsing throws. Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Keep iterator result and decoded message lifetimes within each completed read. Preserve reconnect cursors, parser boundaries and iterator cleanup semantics. Generated-by: OpenAI Codex
Cancel connection-owned channel workers and close the native peer on explicit close, unpolled muxer drop and failed or aborted upgrades. Keep successful connection lifetime separate from signaling-attempt cancellation. Generated-by: OpenAI Codex
Keep retry callbacks outside the delivery lexical context so pending or completed attempts do not retain parsed message bodies for the five-second retry interval. Preserve durable intents, timer behavior, and replay identity. Generated-by: OpenAI Codex
Own the five-minute timer and abort listener in each reconciliation wait. Early public triggers release both immediately while normal expiry and endpoint shutdown still wake the loop. Generated-by: OpenAI Codex
Keep local restart and retry assertions running on Windows without equating stat mode bits with ACL permissions. Production permission handling is unchanged. Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Copy mutable editor state and paste maps without deep-cloning each stored paste for every typed word. Preserve all undo steps and cover public editing, paste renumbering, and retained heap growth. Generated-by: OpenAI Codex
Collect only the code-point prefix needed for nonnegative budgets while preserving cleaning and numeric slice semantics. Cover transient allocations and Unicode behavior with regression tests. Generated-by: OpenAI Codex
Preserve complete redaction and byte-budget semantics while limiting temporary code-point arrays and UTF-8 encoding to the useful prefix. Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Keep startup channel metadata free of Sigstore and TUF module graphs. Load the parser and verifier only when an update is actually verified, preserving fail-closed trust checks. Generated-by: OpenAI Codex
Forward the caller's effective AbortSignal instead of a temporary Request's GC-sensitive derived signal. Preserve explicit null, inherited and overriding signals on every body path, and cover prompt native HTTP cancellation after GC. Generated-by: OpenAI Codex
Encode only the model-visible prefix plus a surrogate guard unit, preserving the byte cap and truncation marker. Cover real Host HTTP fetches, privacy refusal, Unicode boundaries and old-source allocation regression. Generated-by: OpenAI Codex
Clone each runtime resource update once before bounding its state. Preserve source isolation and query revisions, and cover get/list allocation with old-source negative controls. Generated-by: OpenAI Codex
Remove 65 new test and fixture files plus added allocation probes from existing suites. Keep only minimal existing-test behavior and cleanup adaptations. Preserve production memory fixes and remove stale test references. Generated-by: OpenAI Codex
Preserve dynamic plugin prompt composition and share only completed equal base prompts and combined skill inventories. Generated-by: OpenAI Codex
Re-pin the compatible byte-count optimization to main's protocol epoch, record the MCP dependency patch license, and correct the renderer token ledger to the checked source. Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Astro-Han
left a comment
There was a problem hiding this comment.
Reviewed across six dimensions: dependency patches, lifetime/ownership, Rust WebRTC, text truncation, transport framing, Desktop/UI. Differential fuzzing confirmed the 13 rewritten truncation functions and the four framing changes are behaviour-preserving; cargo test webrtc_direct passes. Most of the 64 items are correct equivalent rewrites, and several fix real retention (Path.remainder, the AppShell session→epoch map, the Promise.race reaction in session continuity, zod's lastCtx).
The findings below cannot be fixed at a single line. Line-level ones are filed as inline comments.
P1 — Collapsed reasoning bodies leave the DOM without a decision
packages/ui/src/astryx-chat-reasoning.tsx
Collapse was grid-template-rows: 0fr + overflow: hidden, not display: none. Content stayed in the accessibility tree and stayed findable with Cmd+F. Item 22 removes it until first expansion, so never-opened thinking text is unreachable for screen readers and find-in-page. Streaming collapsed blocks are worse: previewText is gated on !isStreaming, so they expose only "Thinking".
The PR resolved this by editing an existing test to click the header before reading .maka-chat-reasoning-content. That test previously read the text without clicking — the DOM contract changed and the change was absorbed rather than raised.
Per CONTRIBUTING, removing a capability or changing an interaction needs a decision in an issue or Discussion first. Either open one stating the a11y/find-in-page cost this buys, or keep the subtree mounted with hidden="until-found" — which preserves find-in-page but not the memory this item is after.
P1 — Identity comparison for idempotent replay now has two authorities
packages/runtime-host/src/server/message-coordinator.ts, packages/runtime-host/src/server/root-admission-owner.ts
Items 04 and 09 are described as storing less. They also replace the conflict predicate: isDeepStrictEqual(payload) / messageContentsEqual(content) become digest comparison. Getting this wrong either duplicates a delivery or rejects a legitimate resend as operation_conflict — a reconnect-and-resend path.
The two rule sets do line up field for field today. But messageContentsEqual and normalizeMessageContent are written separately in events.ts, and nothing binds them. Editing either silently breaks idempotent replay with no failing test.
Make messageContentsEqual call messageContentDigest on both sides so there is one rule. That also removes the superseded predicate rather than leaving both.
P2 — Releasing a reference and closing in order are treated as one primitive
Two items make the same mistake in opposite directions.
Item 06, packages/runtime/src/stream-graph-coordinator.ts:1453 — #retireDriver awaits driver.stopTask, the operator teardown I/O, with no timeout and no abort (beginDrain's abort only reaches driver.abortController). Its only production caller is prepareFreshAgentGraphEpoch, so every new Turn in graph/swarm mode now starts behind the previous epoch's shutdown. Hang one operator in teardown and the next Turn cannot start; on main the epoch advances regardless.
Item 13, native/runtime-host-peer/src/webrtc_direct/lifetime.rs:48 — close() cancels before peer_connection.close(), cutting off workers still inside drive_data_channel and discarding both the per-substream outgoing queue and the up-to-5s drain_data_channel flush. On main, close and the workers ran concurrently, so drain could complete.
Dropping a reference is synchronous, has no side effects, and can always happen immediately. Closing in order is asynchronous, does I/O, and can fail or hang. Item 06 waited for a close under the name of releasing; item 13 cancelled under the name of closing. Both are one line, but fixing them separately leaves the distinction unstated.
void this.#retireDriver(driver) works for 06 — Promise.allSettled already absorbs the errors — or bound the stopTask wait. For 13, move the cancel after peer_connection.close().await; Drop should keep cancelling first, since that path has no graceful-close semantics.
P2 — A shared invariant is rederived at 13 call sites
Items 42–63 all rest on the same fact: a code point encodes to at least one UTF-8 byte, so collecting budget + 1 UTF-16 units before slicing by bytes is safe. Every derivation is correct — but each is local.
The cost is already visible: tool-output.ts guards degenerate budgets to preserve Buffer.subarray's negative-end semantics, web-fetch-tool.ts uses the same technique without the guard. Unreachable today because the budget is a constant; it is what rederiving the same reasoning 13 times produces.
text-line-window.ts is the counter-example — it merges two duplicated line-window implementations into one authority. Same treatment for a boundedPrefixByBytes(text, maxBytes) would let most of these call sites shrink to one call. A smaller instance: use-composer-history.ts fixes a non-lazy useRef, use-composer-attachments.ts keeps one; a shared useLazyRef covers both.
P2 — Canonical-base64 validation and length measurement are separable
Buffer.byteLength(s, 'base64') equals the decoded length only for canonical input — Node computes (len - padding) * 3 >>> 2 without checking the alphabet, whitespace, or a multiple-of-four length. All four call sites validate first, so item 55 is correct as written.
Nothing in the signature carries that precondition, and the input is peer-controlled. A future call site that skips validation lets a peer pass SESSION_TRANSCRIPT_PAGE_MAX_BYTES while decoding to a different size. A canonicalBase64ByteLength(value) that validates and measures together removes the gap and drops one duplicated regex.
P2 — The PR's central premise is an unspecified engine detail, stated five times
Five sites rely on structuredClone(<string>) to detach a slice from an oversized backing string. That is a V8 value-serializer behaviour, not a language guarantee, and items 23/33/35/36/37/39 all depend on it.
Right now the same justification is repeated in five comments with no measurement. One detachString(s) helper, explained once, plus a single retained-size heap snapshot in the PR body would make the premise checkable.
P2 — The tests for these changes exist in this branch's history
44bc559f6 removes 65 test files and 11,509 lines. That includes the behaviour-asserting ones this review would otherwise ask for: zod-memoizer-lifetime.test.ts, webrtc_direct/lifecycle_tests.rs, request-customization-abort.test.ts, goal-token-lifetime.test.ts, stream-graph-coordinator-retirement.test.ts, computer-use-session-lifetime.test.ts, file-read-line-window-allocation.test.ts, edit-diff-window-allocation.test.ts.
Dropping the allocation probes is reasonable — they are unstable across Node versions and Electron, which the earlier make prompt heap checks portable to Electron commit already shows. The eight above assert behaviour, not allocation counts, and fail on the old code. Restoring them from 44bc559f6^ costs far less than rewriting them.
P3 — Three body entries do not match the diff
- 07 describes connection-lifetime promise waits becoming removable close listeners. The diff for
session-transcript.tsandsession-transcript-pager.tscontains only theBuffer.byteLengthchange. - 31/32 describe sharing frozen snapshots and completed prompt strings. The actual change bounds code-point array allocation.
- 27 omits that
applyServiceReleaseno longer notifiesonSessionInvalidatedfor locally unknown sessions.
In a body that enumerates 64 items, mismatches cost the reader confidence in the other 61.
P3 — Three behaviour fixes are filed as allocation work
Item 25 restores cancellation through request customization, item 27 adds the known gate, and the two identity changes above alter a predicate. Each is a fix worth having and worth its own commit and body entry; under "reduce redundant allocations" they get skimmed.
P3 — The three new patches have no upstream exit
patches/README.md entries reference upstream issues elsewhere (See #2978, See #1967 / #1976, See #3446). The zod, MCP and pi-tui entries have none, and their stated exit conditions describe upstream behaviour changes that nobody upstream has been asked for. The zod parse-state leak and the MCP settled-observer retention are general bugs, not Maka-specific needs.
中文
从六个维度评审:依赖补丁、生命周期与所有权、Rust WebRTC、文本截断、传输分帧、Desktop/UI。差分 fuzz 确认 13 个被重写的截断函数和四处分帧改动行为不变,cargo test webrtc_direct 通过。64 项里大部分是正确的等价改写,其中几项修的是真实驻留(Path.remainder、AppShell 的 session→epoch map、session continuity 的 Promise.race reaction、zod 的 lastCtx)。
以下问题无法在单行修复,能局部修的已作为行内评论提出。
P1 — 折叠态 reasoning 正文离开 DOM,且未经定案
packages/ui/src/astryx-chat-reasoning.tsx
折叠原本是 grid-template-rows: 0fr + overflow: hidden,不是 display: none,内容留在可访问性树里、能被 Cmd+F 命中。第 22 项改成首次展开才挂载,未展开的思考文本对屏幕阅读器和页内查找不可达。流式折叠块更差:previewText 带 !isStreaming 条件,只剩一个 "Thinking"。
PR 的处理方式是改一个既有测试,让它先点 header 再读 .maka-chat-reasoning-content。那个测试原来不点击就能读到文本——DOM 契约变了,改动被吸收而不是被提出。
按 CONTRIBUTING,删功能或改交互须先在 issue/Discussion 定案。要么开一个说明这是拿 a11y 和查找可达性换内存,要么用 hidden="until-found" 保留挂载——但那样保不住这一项想要的内存。
P1 — 幂等重放的身份比较现在有两个权威
packages/runtime-host/src/server/message-coordinator.ts、packages/runtime-host/src/server/root-admission-owner.ts
04 和 09 的描述是少存正文,实际还换了冲突判据:isDeepStrictEqual(payload) / messageContentsEqual(content) 变成摘要比较。判错一边是重复投递,另一边是把合法重发误判为 operation_conflict——断线重连重发是正常路径。
两套规则今天逐字段对齐。但 messageContentsEqual 和 normalizeMessageContent 在 events.ts 里各写各的,没有任何东西绑定它们,改动任一边都会静默打破幂等重放且没有测试会失败。
让 messageContentsEqual 两边都调 messageContentDigest,规则只剩一份,也顺带删掉被替代的判据。
P2 — 「放弃引用」和「有序关闭」被当成同一个原语
两项在相反方向上犯了同一个错误。
第 06 项,packages/runtime/src/stream-graph-coordinator.ts:1453——#retireDriver 等 driver.stopTask,那是 operator 的 teardown I/O,没有超时也没有 abort(beginDrain 的 abort 只作用于 driver.abortController)。它唯一的生产调用方是 prepareFreshAgentGraphEpoch,于是 graph/swarm 模式下每个新 Turn 都排在上一个 epoch 的关闭之后。某个 operator 卡在 teardown,下一个 Turn 就起不来;main 上 epoch 会直接推进。
第 13 项,native/runtime-host-peer/src/webrtc_direct/lifetime.rs:48——close() 在 peer_connection.close() 之前 cancel,掐断仍在 drive_data_channel 里的 worker,丢掉 per-substream 的 outgoing 队列和最长 5s 的 drain_data_channel flush。main 上 close 和 worker 是并发的,drain 有机会跑完。
放弃引用是同步的、无副作用的、永远可以立即做;有序关闭是异步的、有 I/O、可能失败也可能卡住。第 06 项以释放的名义等了关闭,第 13 项以关闭的名义做了取消。两处都只是一行,但分开修就说不出这个区别。
06 用 void this.#retireDriver(driver) 即可(Promise.allSettled 已经吞掉异常),或给 stopTask 的等待加上界。13 把 cancel 移到 peer_connection.close().await 之后;Drop 里保持先 cancel,那条路径本来就没有优雅关闭语义。
P2 — 同一条不变量在 13 个调用点各推导一次
42–63 全部建立在同一个事实上:一个码点至少编码成一个 UTF-8 字节,所以先收 budget + 1 个 UTF-16 单元再按字节切是安全的。每处推导都对,但都是局部的。
代价已经出现:tool-output.ts 为退化预算加了守卫以保留 Buffer.subarray 负数 end 的语义,web-fetch-tool.ts 同样的技巧没加。今天不可达(预算是常量),但这就是同一推理写 13 遍的结果。
text-line-window.ts 是反例——它把两处重复的行窗口实现合并成一个权威。给 boundedPrefixByBytes(text, maxBytes) 同样处理,多数调用点能缩成一次调用。小号版本:use-composer-history.ts 修了非惰性 useRef,use-composer-attachments.ts 留着;一个共享的 useLazyRef 覆盖两处。
P2 — 规范 base64 的校验与测长可以被分开
Buffer.byteLength(s, 'base64') 只在输入规范时等于解码长度——Node 算的是 (len - padding) * 3 >>> 2,不检查字母表、空白和长度是否 4 的倍数。四个调用点都先校验,所以第 55 项本身是对的。
但签名里没有携带这个前置条件,而输入是 peer 可控的。将来某个跳过校验的调用点会让 peer 通过 SESSION_TRANSCRIPT_PAGE_MAX_BYTES 而实际解码出别的长度。一个 canonicalBase64ByteLength(value) 把校验和测长写在一起即可消除这个缺口,顺带删掉一处重复正则。
P2 — PR 的核心前提是一个未规范化的引擎细节,且被陈述了五遍
五处依赖 structuredClone(<string>) 把切片从超大 backing string 上摘下来。这是 V8 value-serializer 的行为,不是语言保证,而 23/33/35/36/37/39 全部依赖它。
现在同一个理由在五条注释里各写一遍,没有测量。一个 detachString(s)、解释一次,加上 PR 正文里一次 retained size 的堆快照,这个前提就可核对了。
P2 — 这些改动的测试就在本分支的历史里
44bc559f6 删掉 65 个测试文件、11509 行,其中包括本次评审本来要求补的那些行为断言型测试:zod-memoizer-lifetime.test.ts、webrtc_direct/lifecycle_tests.rs、request-customization-abort.test.ts、goal-token-lifetime.test.ts、stream-graph-coordinator-retirement.test.ts、computer-use-session-lifetime.test.ts、file-read-line-window-allocation.test.ts、edit-diff-window-allocation.test.ts。
删掉 allocation probe 是合理的——它们跨 Node 版本和 Electron 不稳定,之前那个 make prompt heap checks portable to Electron commit 已经说明了。但上面八个断言的是行为不是分配量,在旧代码上会失败。从 44bc559f6^ 挑回来,成本远低于重写。
P3 — 正文三处与 diff 不符
- 07 描述连接级 promise 等待改成可移除的 close 监听。
session-transcript.ts和session-transcript-pager.ts的 diff 里只有Buffer.byteLength替换。 - 31/32 描述共享冻结快照和已完成 prompt 字符串,实际改动是码点数组的分配上限。
- 27 漏掉了
applyServiceRelease不再为本地未知会话通知onSessionInvalidated。
在一份逐条列举 64 项的正文里,对不上的条目会让读者对其余 61 条失去信心。
P3 — 三处行为修复被归在分配优化下
第 25 项恢复了请求定制路径的取消,第 27 项加了 known 门,上面两处身份比较换了判据。每一项都值得做,也都值得有自己的 commit 和正文条目;放在「减少重复分配」下会被略过。
P3 — 三个新补丁没有上游出口
patches/README.md 里既有条目都引用了上游(See #2978、See #1967 / #1976、See #3446)。zod、MCP 和 pi-tui 三条都没有,而它们写的退出条件描述的是上游的行为变化,但没人向上游提过。zod 的解析状态泄漏和 MCP 的已结算观察者驻留都是通用 bug,不是 Maka 特有需求。
| return; | ||
| } | ||
|
|
||
| const require = createRequire(import.meta.url); |
There was a problem hiding this comment.
P1 — packages/runtime/src/bots/slack-bridge.ts:90
(same at feishu-bridge.ts:99, wecom-bridge.ts:94)
The require sits outside the try that follows. A resolution failure — trimmed install, missing nested @slack/socket-mode — now throws out of start(), skipping recordFailure, readiness = 'degraded' and emitStatusChange(). Before item 02 this failed at module-graph load: once, at startup, loudly. Now it fails when the user clicks connect and the bot status never updates.
bot-test.ts:97 puts its require inside the try. Move these three in as well.
中文
require 在随后的 try 之外。模块解析失败(安装被裁、嵌套的 @slack/socket-mode 缺失)会裸抛出 start(),跳过 recordFailure、readiness = 'degraded' 和 emitStatusChange()。第 02 项之前这是模块图加载期的失败:启动时一次性、明显。现在变成用户点连接才失败,而 bot 状态不更新。
bot-test.ts:97 的 require 就在 try 里,这三处照做。
There was a problem hiding this comment.
English
Accepted and fixed in 4aa979987. SDK resolution and client construction now run inside each bridge's failure boundary, including the case where no client was constructed. Loading remains synchronous, so this does not add a stop/loading await race.
The registry regression forces the three SDK resolutions to fail and checks the emitted failure statuses. It fails against the pre-follow-up source and passes now. Slack retains its existing degraded/rethrow behavior; Feishu and WeCom retain their configured failure state. This is a connection-failure handling defect, not a failure of every normally installed connection.
Automated follow-up by OpenAI Codex.
中文
接受,已在 4aa979987 修复。SDK 解析与客户端构造现已纳入各 bridge 的失败处理边界,也覆盖尚未构造客户端就失败的情况。加载仍同步执行,没有新增 stop 与模块加载 await 的竞态。
回归测试通过 registry 强制三种 SDK 解析失败,并检查实际发出的失败状态;在修复前源码上失败,当前通过。Slack 保留原有 degraded/重新抛出行为,飞书和企业微信保留 configured 失败状态。这是连接失败处理缺陷,不代表正常安装下的所有连接都会失败。
OpenAI Codex 自动跟进回复。
| @@ -608,13 +608,9 @@ export function createWorkHubPresentation(deps: WorkHubPresentationDeps) { | |||
| const enabled = deps.isEnabled(); | |||
| if (disposed) return; | |||
| if (enabled) { | |||
There was a problem hiding this comment.
P1 — apps/desktop/src/main/workhub-presentation.ts:610
detach() now runs ensureView() → show() → focusComposer(), and focusComposer returns early on !rendererReady, leaving only focusPending. The floating window is transparent: true, backgroundColor: '#00000000', so until the renderer loads the user sees an empty rounded frame with no loading state, and keystrokes in that window are dropped because webContents has no focus.
The main-window path calls ensureView() when the dock is visible, but the shortcut path deliberately bypasses the main window — the test at :571 asserts mainRequests === 0. So "Desktop closed, press the shortcut" has no warm-up at all.
requestProgress() / progress-ready already implements "renderer confirms it painted, then showInactive". Reuse it, or keep a prewarm triggered on first shortcut registration rather than removing it.
中文
detach() 现在是 ensureView() → show() → focusComposer(),而 focusComposer 在 !rendererReady 时直接 return,只置 focusPending。floating 窗口是 transparent: true, backgroundColor: '#00000000',渲染器加载完之前用户看到一个空的圆角框,没有 loading 态,期间的键入因 webContents 未 focus 而丢失。
主窗口路径在 dock 可见时会调 ensureView(),但快捷键路径刻意绕开主窗口——:571 的测试断言 mainRequests === 0。所以「Desktop 没开时按快捷键」完全没有预热。
requestProgress() / progress-ready 已经实现了「渲染器确认画完再 showInactive」。复用它,或者把 prewarm 改成首次注册快捷键后触发而不是删掉。
There was a problem hiding this comment.
English
Accepted the cold-start presentation gap; fixed in e2721989f without restoring background prewarming.
A cold floating summon stays hidden until the existing renderer-ready handshake, then shows and focuses the mounted composer. The existing focusPending state owns that pending summon: a second shortcut or disabling cancels it, and passive progress cannot replace it. Once ready, warm shortcut show/hide remains synchronous.
All 24 presentation tests pass, including no renderer from enabling alone, no window focus during cold loading, ready-triggered presentation, cancellation before ready, and the existing docking/progress/animation behavior. This validates the controller behavior, not an all-platform packaged-app cold-start latency measurement.
Automated follow-up by OpenAI Codex.
中文
接受冷启动展示空档的问题,已在 e2721989f 修复,不恢复后台预热。
冷启动浮窗会保持隐藏,直到现有 renderer-ready 握手完成,再显示并聚焦已挂载的输入框。复用 focusPending 管理待显示请求:第二次快捷键或禁用可以取消它,被动进度展示也不能替换它。就绪后的快捷键显示/隐藏仍同步完成。
24 项展示控制测试全部通过,包括仅启用不创建 renderer、冷加载不抢焦点、ready 后展示、ready 前取消,以及原有停靠/进度/动画行为。这是控制器行为验证,不冒称测量了所有平台打包应用的冷启动耗时。
OpenAI Codex 自动跟进回复。
| ]; | ||
| for (const sessionId of sessions) { | ||
| // Unknown cleanup must not create tool-layer state through the observer. | ||
| const known = |
There was a problem hiding this comment.
P2 — packages/computer-use/src/maka-cu-backend.ts:835
Scoping sessionGenerations to live operations is equivalent — I traced the fence both ways. But the known gate in the same hunk is a separate behaviour change: applyServiceRelease used to call opts.onSessionInvalidated unconditionally and now skips locally unknown sessions. Reachable after clearSession(S) has removed S from begunSessions, when a maka-cu crash fires generationReleased with S still in sessionIds.
The comment states this as a bug being fixed, and I think the new behaviour is right. It needs a regression test: begin a session, run an action to completion so sessionGenerations empties, then deliver a generationReleased release naming it, and assert the onSessionInvalidated call count against the new contract.
中文
把 sessionGenerations 收敛到在飞操作是等价的,fence 的两个方向我都推过。但同一 hunk 里的 known 门是独立的行为改动:applyServiceRelease 原来无条件调 opts.onSessionInvalidated,现在对本地未知的会话跳过。可达路径:clearSession(S) 已把 S 从 begunSessions 移除后,maka-cu 崩溃触发 generationReleased 且 sessionIds 里仍带 S。
注释把它写成在修一个 bug,我认为新行为是对的。它需要一个回归测试:begin 一个会话,跑一个 action 到结束使 sessionGenerations 清空,然后投递一个指名该会话的 generationReleased release,按新契约断言 onSessionInvalidated 的调用次数。
There was a problem hiding this comment.
English
The notification boundary is intentional, and it is now explicit in item 27. 4bbe8fd3d extends the existing backend lifecycle test rather than restoring a separate allocation-probe file.
It checks that clearing a known session notifies once, clearing it again or clearing a never-begun session does not notify, and a still-known session whose observation has completed is invalidated when the owned mock executor is killed. The 57 backend tests pass.
One correction to the proposed scenario: completion empties the operation fence but does not remove begunSessions. The locally unknown case needs a clear/retirement step; begin → completed action → generationReleased should still notify.
Automated follow-up by OpenAI Codex.
中文
通知边界是有意修改,现已在第 27 项明确写出。4bbe8fd3d 扩充现有 backend 生命周期测试,没有恢复独立的分配量探针文件。
测试确认:清理已知会话通知一次;重复清理或清理从未 begin 的会话不再通知;已完成 observation、但仍已知的会话,在测试自有 mock executor 被终止后仍会失效通知。57 项 backend 测试通过。
建议场景有一点需要修正:操作结束会释放 operation fence,但不会删除 begunSessions。本地未知场景需要 clear/retirement;begin → 操作完成 → generationReleased 仍然应当通知。
OpenAI Codex 自动跟进回复。
| } | ||
| } | ||
|
|
||
| private async processNextLiveMessages( |
There was a problem hiding this comment.
P2 — packages/runtime/src/bots/wechat-bridge.ts:214
This file is +87/−27, far beyond the other three bridges, and the SSE rewrite has nothing to do with lazy SDK loading. It is ~70 lines reimplementing for await's completion protocol by hand.
The protocol is correct — closeIterator is false when next() rejects, true when the yield throws, cleared after the catch closes, and readSseTextChunk closes itself on a decode throw. No double close, no leaked iterator. But the claimed gain is a comment about V8 frame liveness with no heap snapshot behind it. Removing processNextLiveMessages / readSseTextChunk / closeIterator and restoring the two for await loops loses nothing measurable against this PR's stated goal.
Either attach a before/after snapshot for a bot process holding large SSE chunks, or revert this file's streaming rewrite. Either way it does not belong inside item 02.
中文
这个文件 +87/−27,远超其他三个 bridge,而 SSE 重写和 SDK 惰性加载毫无关系。它是约 70 行手工重实现 for await 的完成协议。
协议本身是对的——next() reject 时 closeIterator 为 false,yield 抛出时为 true,catch 关闭后置回 false,readSseTextChunk 在 decode 抛出时自行关闭。没有双重关闭,没有泄漏的 iterator。但声称的收益只是一条关于 V8 栈帧活跃度的注释,背后没有堆快照。删掉 processNextLiveMessages / readSseTextChunk / closeIterator 并恢复两处 for await,按本 PR 的既定目标衡量没有任何可度量的损失。
要么附上 bot 进程持有大 SSE chunk 时的前后快照,要么回退本文件的流式重写。无论哪种,它都不属于第 02 项。
There was a problem hiding this comment.
English
Keeping the streaming rewrite. It is item 12, introduced separately by d02caf9, not part of SDK-loading item 02.
A fresh Node 24.18.1 A/B run of the actual readSseJsonObjects source disproves “nothing measurable”: after consuming an 8 MiB SSE byte chunk and waiting for the next chunk, arrayBuffers is 8,535,450 bytes before vs 146,823 bytes after (both start at 146,823). Ending the old stream returns it to baseline. This is last-chunk idle retention, not a growing history or an RSS claim.
The comparison uses a controlled AsyncIterable and forced GC, not a live WeChat deployment. It isolates the source before d02caf9 against the current implementation. Since the iterator completion behavior is also correct, reverting would reintroduce the measured retention.
Automated follow-up by OpenAI Codex.
中文
保留流式读取重写。它属于第 12 项,由 d02caf9 独立引入,不属于 SDK 加载的第 02 项。
重新在 Node 24.18.1 上对实际 readSseJsonObjects 源码做 A/B:消费一个 8 MiB SSE 字节块、等待下一块时,arrayBuffers 为修改前 8,535,450 字节、修改后 146,823 字节,两者起点均为 146,823。旧流结束后回到起点。因此“没有可度量损失”不成立;这是末块空闲驻留,不是历史持续增长,也不是 RSS 节省承诺。
该对照使用受控 AsyncIterable 和强制 GC,不是真实微信部署;比较 d02caf9 之前的源码与当前实现。迭代器完成协议也正确,回退会重新引入已测出的驻留。
OpenAI Codex 自动跟进回复。
| @@ -208,6 +212,20 @@ export function useComposerAttachments(options: { | |||
| // Live mirror of every staged item's key, for async preview arrivals to | |||
| // check before writing: state snapshots inside a .then are stale by design. | |||
| const lifecycleRef = useRef(new ComposerAttachmentLifecycle()); | |||
There was a problem hiding this comment.
P2 — packages/ui/src/use-composer-attachments.ts:214
useRef's argument is not lazy: every render constructs a ComposerAttachmentLifecycle — a Set, a Map, a private Set — and discards it. This PR adds readonly previewUrls = new Map() to that class, so each keystroke in the composer now produces more garbage than before.
use-composer-history.ts fixes exactly this pattern in the same PR. Use useRef<ComposerAttachmentLifecycle | null>(null) with lazy assignment, or a shared useLazyRef for both sites.
中文
useRef 的参数不是惰性的:每次渲染都构造一个 ComposerAttachmentLifecycle(一个 Set、一个 Map、一个私有 Set)然后丢弃。本 PR 给这个类新增了 readonly previewUrls = new Map(),于是 composer 里每次按键产生的垃圾比改前更多。
同一个 PR 的 use-composer-history.ts 修的正是这个模式。用 useRef<ComposerAttachmentLifecycle | null>(null) 加惰性赋值,或者给两处共用一个 useLazyRef。
There was a problem hiding this comment.
English
Accepted and fixed in 4bbe8fd3d: the lifecycle now uses a mount-scoped lazy initializer:
const [lifecycle] = useState(() => new ComposerAttachmentLifecycle());Its mutable containers and existing cleanup behavior are unchanged; unrelated renders no longer construct a discarded lifecycle. No shared useLazyRef abstraction was added. The complete UI suite passes: 430 tests.
Automated follow-up by OpenAI Codex.
中文
接受,已在 4bbe8fd3d 修复,改用挂载期惰性初始化:
const [lifecycle] = useState(() => new ComposerAttachmentLifecycle());可变容器及既有清理行为保持不变,普通重新渲染不再构造并丢弃 lifecycle。没有新增通用 useLazyRef 抽象。完整 UI 套件 430 项通过。
OpenAI Codex 自动跟进回复。
| for (const listener of this.#endListeners) listener(); | ||
| return; | ||
| } | ||
| this.#emitData(chunk); |
There was a problem hiding this comment.
P2 — packages/runtime-host/src/transport/peer-native.ts:501
(same at resumable-peer-stream.ts:277, :295)
chunk is declared inside the while body, so each iteration has its own binding and it is unreachable once the iteration ends; the next await happens under a fresh binding. The only closure in the peer-native loop captures a native promise resolve, which does not pin the block context. These three assignments are no-ops.
The comment backs them up — "drop consumed chunks" describes the no-op. The other half, the Buffer.alloc(0) tail replacement, is real. So is initialData = Buffer.alloc(0) at :487, where initialData is a parameter alive across the loop's awaits; that one is load-bearing and worth its comment.
Drop the three assignments (let back to const) and narrow the comment to the tail replacement.
中文
chunk 声明在 while 体内,每轮迭代是独立绑定,迭代结束即不可达;下一次 await 发生在新绑定下。peer-native 这个循环里唯一的闭包捕获的是原生 promise 的 resolve,不会钉住块上下文。这三个赋值是 no-op。
注释为它们做了背书——"drop consumed chunks" 说的就是这个 no-op。另一半,Buffer.alloc(0) 的尾部替换,是真的。:487 的 initialData = Buffer.alloc(0) 也是真的,那里 initialData 是形参,跨循环的多次 await 存活;那行是 load-bearing 的,值得有注释。
删掉三个赋值(let 改回 const),把注释收窄到尾部替换。
P3 — packages/runtime-host/src/transport/peer-native.ts:413
remainder: buffered.subarray(newline + 1) is the zero-length-view pattern item 50 fixes. Most handshakes have no trailing bytes, so this pins the handshake backing store (up to AUTHENTICATION_MAX_BYTES).
Bounded and short-lived — #pump replaces it with Buffer.alloc(0) immediately, and #pumpRead drops it on the first Buffer.concat — so not a leak. But the same length === n ? Buffer.alloc(0) : subarray(n) form is applied at two sibling sites; applying it here keeps them consistent.
中文
remainder: buffered.subarray(newline + 1) 正是第 50 项修的那个零长视图模式。多数握手没有尾随字节,所以这里钉住整个握手 backing store(上限 AUTHENTICATION_MAX_BYTES)。
有界且短命——#pump 立刻用 Buffer.alloc(0) 替换,#pumpRead 在第一次 Buffer.concat 时丢掉——所以不是泄漏。但同样的 length === n ? Buffer.alloc(0) : subarray(n) 已经用在两个兄弟位置,这里也照做才一致。
There was a problem hiding this comment.
English
Keeping all three clear assignments: lexical block scope does not guarantee that V8 clears a suspended async frame's backing references.
On Node 24.18.1, actual-source A/B runs remove the assignments and change let back to const, leaving the remainder fixes intact:
| Consumed/idle state | Current | Requested removal |
|---|---|---|
| Resumable DATA, consumed and ACKed | native 65,536-byte block collected | retained |
| Partial frame completed and consumed | prior 64,000-byte block collected | retained |
| RuntimeHostPeerByteStream, onData completed | 65,536-byte block collected | retained |
These use controlled native-stream reads, WeakRefs and forced GC; legitimate unread/replay buffers remain owned in both variants. The assignments therefore are not no-ops on the target engine.
Accepted the separate empty authentication-tail improvement in 4bbe8fd3d: a fully consumed line now returns Buffer.alloc(0), while nonempty remainder bytes are preserved. The 14 peer transport tests pass.
Automated follow-up by OpenAI Codex.
中文
保留三处清空赋值:词法块作用域不能保证 V8 已清除挂起 async 栈帧中的底层引用。
在 Node 24.18.1 上使用实际源码做 A/B,删除赋值并将 let 改回 const,其他 remainder 修复保持不变:
| 已消费/空闲状态 | 当前实现 | 按建议删除后 |
|---|---|---|
| Resumable DATA 已消费并 ACK | 原生 65,536 字节块已回收 | 仍保留 |
| 部分帧完成且已消费 | 先前 64,000 字节块已回收 | 仍保留 |
| RuntimeHostPeerByteStream 的 onData 已结束 | 65,536 字节块已回收 | 仍保留 |
实验使用受控原生流读取、WeakRef 与强制 GC;合法的未读/重放缓冲在两版中都继续保留。因此这些赋值在目标引擎上并非 no-op。
另一条认证空尾建议已接受,在 4bbe8fd3d 中,整行消费完改为返回 Buffer.alloc(0),非空 remainder 字节保持不变。14 项 Peer 传输测试通过。
OpenAI Codex 自动跟进回复。
|
|
||
| // Match split('\n').slice(offset, end).join('\n'), without creating entries | ||
| // for lines outside the requested window. Empty and trailing lines count too. | ||
| export function readTextLineWindow(content: string, offset?: number, limit?: number): string { |
There was a problem hiding this comment.
P3 — packages/runtime/src/text-line-window.ts:22
The comment says it matches split('\n').slice(offset, end).join('\n') but not where end comes from. const end = limit ? start + limit : lineCount treats limit === 0 as unbounded and returns everything after offset. Both current call sites already had that falsy semantic, so this is not a regression — but this is now a shared authority, and someone reading the comment will write readTextLineWindow(content, 0, 0) expecting an empty string.
Document the limit === 0 case, or switch to limit === undefined ? lineCount : start + limit after checking whether either call site's schema permits 0.
中文
注释说它等价于 split('\n').slice(offset, end).join('\n'),但没说 end 从哪来。const end = limit ? start + limit : lineCount 把 limit === 0 当作不限,返回 offset 之后的全部内容。两个现有调用点本来就是这个 falsy 语义,所以不是回归——但它现在是共享权威,照注释理解的人会写 readTextLineWindow(content, 0, 0) 并期望空串。
把 limit === 0 的语义写进注释,或者改成 limit === undefined ? lineCount : start + limit,改之前先确认两个调用点的 schema 是否允许 0。
There was a problem hiding this comment.
English
Accepted the documentation fix in 4bbe8fd3d. The shared helper now explicitly states that an omitted or zero limit is unbounded and end is the total line count. Kept the existing call-site semantics; did not change zero into an empty result.
Automated follow-up by OpenAI Codex.
| const inflight = pendingAcquires.get(sessionId); | ||
| if (inflight) return inflight; | ||
| const epoch = 0; | ||
| // Register before resolveEndpoint, which may synchronously release the session |
There was a problem hiding this comment.
P3 — apps/desktop/src/main/browser/session.ts:249
"Register before resolveEndpoint, which may synchronously release the session before this attempt can be registered in pendingAcquires" — the old code handled that too: epoch stays undefined, a synchronous release sets it to 1, and 1 !== undefined unwinds. The real motivation is that releaseEpochs kept one never-cleared entry per session.
The change itself is fine — there is no await between releaseEpochs.set and pendingAcquires.set, and not bumping with no acquire in flight is safe since the only reader is an in-flight acquire. Just make the comment describe the map's new lifetime instead of a correctness reason that does not hold.
中文
"Register before resolveEndpoint, which may synchronously release the session before this attempt can be registered in pendingAcquires"——旧代码同样处理得了:epoch 保持 undefined,同步 release 置 1,1 !== undefined 照样 unwind。真实动机是 releaseEpochs 每个会话留一条永不清理的记录。
改动本身没问题——releaseEpochs.set 到 pendingAcquires.set 之间没有 await,无在飞 acquire 时不 bump 也安全,因为唯一的读者就是在飞的 acquire。只是把注释改成描述 map 的新生命周期,而不是一个不成立的正确性理由。
There was a problem hiding this comment.
English
Keeping this comment. Agreed that the old implementation also handled synchronous release, and that the optimization removes historical map entries. The comment is not claiming an old correctness bug: it records an ordering requirement of the new pending-acquire-only representation.
Because release only advances a registered in-flight epoch now, registration must happen before a reentrant resolveEndpoint call, not after it. Describing that required order remains useful even though both implementations are correct. No functional change requested here is needed.
Automated follow-up by OpenAI Codex.
中文
保留这条注释。同意旧实现也能处理同步 release,也同意优化目的是移除历史 map 项。但注释没有声称旧实现存在正确性缺陷,而是在记录新“仅待决 acquire 持有状态”表示的顺序要求。
现在 release 只推进已注册的在飞 epoch,因此必须在可能重入的 resolveEndpoint 调用之前注册,不能放到之后。两版都正确,不影响该顺序说明仍有价值;这里无需功能修改。
OpenAI Codex 自动跟进回复。
| test('prewarms once and the shortcut shows and hides synchronously', async () => { | ||
| test('creates on first shortcut, then shows and hides synchronously', async () => { | ||
| const h = await harness(); | ||
| await h.controller.refreshSettings(); |
There was a problem hiding this comment.
P3 — apps/desktop/src/main/__tests__/workhub-presentation.test.ts:555
The rename to creates on first shortcut has nothing asserted between refreshSettings() and the first h.shortcut(). Item 01's only behavioural claim — enabling alone must not create the renderer — has no regression cover; re-adding prewarm leaves this test green.
assert.equal(h.views.length, 0, 'enabling alone must not create the renderer'). Same shape as the assertions at :492 and :508.
中文
改名成 creates on first shortcut,但 refreshSettings() 和第一次 h.shortcut() 之间什么都没断言。第 01 项唯一的行为主张——启用本身不创建渲染器——没有回归保护,把 prewarm 加回来这个测试照样绿。
加 assert.equal(h.views.length, 0, 'enabling alone must not create the renderer'),和 :492、:508 的断言同形状。
There was a problem hiding this comment.
English
Accepted in e2721989f. The existing test now asserts views.length === 0 immediately after refreshSettings and before the first shortcut. It also checks that cold loading does not show/focus the window until ready, while subsequent warm toggles remain synchronous. All 24 presentation tests pass.
Automated follow-up by OpenAI Codex.
中文
接受,已在 e2721989f 的现有测试中,在 refreshSettings 后、第一次快捷键前断言 views.length === 0。还检查冷加载期间不显示/聚焦窗口、ready 后展示,以及后续热启动切换仍同步完成。24 项展示控制测试通过。
OpenAI Codex 自动跟进回复。
| return new Promise<T>((resolve, reject) => { | ||
| const onClose = () => reject(closed.reason); | ||
| if (closed.aborted) onClose(); | ||
| else closed.addEventListener('abort', onClose, { once: true }); |
There was a problem hiding this comment.
P3 — packages/runtime-host/src/server/session-continuity-coordinator.ts:2270
Good catch on the underlying issue: Promise.race([prepared, connection.closed.then(...)]) attaches a reaction to connection.closed that retains the derived promise's resolving functions, the race result and its fulfillment value — every winning overlay until disconnect.
Two small things. if (closed.aborted) onClose(); else addEventListener(...) falls through after onClose(), running void task.then(...) and a removeEventListener for a listener never registered; onClose(); return; reads better and behaves the same. And #closeConnection aborts with a single Error instance shared by every waiter on that connection, so stacks point at #closeConnection rather than the await site — the old code constructed one per race.
中文
底层问题抓得好:Promise.race([prepared, connection.closed.then(...)]) 在 connection.closed 上挂一条 reaction,持有派生 promise 的 resolving functions、race 结果及其 fulfillment value——也就是每个 winning overlay,直到断连。
两点小问题。if (closed.aborted) onClose(); else addEventListener(...) 在 onClose() 后继续往下走,执行 void task.then(...) 和一次针对从未注册的监听器的 removeEventListener;onClose(); return; 更好读且行为相同。另外 #closeConnection 用单个 Error 实例 abort,该连接上所有等待者共享它,栈指向 #closeConnection 而非等待点——旧代码是每次 race 现场新建。
There was a problem hiding this comment.
English
Not adopting the early return: it is not behavior-equivalent. task is an already-started Promise. Even when closed is already aborted, its rejection must still be observed by task.then.
An A/B run of this actual helper with an already-aborted signal and Promise.reject(new Error('task failed')) produces no unhandledRejection currently; adding the proposed return produces unhandledRejection: task failed. Removing a listener that was never registered is harmless.
Keeping one connection close reason as well. It identifies the shared close cause; the old per-race Error was created in a Promise reaction, not inherently at the caller's await site. This is a diagnostic trade-off, not a demonstrated correctness regression.
Automated follow-up by OpenAI Codex.
中文
不采纳提前 return,因为它并不行为等价。task 是已经启动的 Promise;即使 closed 已经 aborted,仍需通过 task.then 接住任务的拒绝。
对实际 helper 做 A/B:传入已 aborted 的 signal 与 Promise.reject(new Error('task failed')),当前没有 unhandledRejection;加上建议的 return 后出现 unhandledRejection: task failed。移除一个未注册过的 listener 则无害。
同时保留连接共享的关闭原因,用于标识共同的关闭事件。旧版每个 race 的 Error 在 Promise reaction 中创建,并不天然就是调用方 await 位置的堆栈。这属于诊断取舍,没有证实正确性回归。
OpenAI Codex 自动跟进回复。
Fence retired drivers synchronously, then release their snapshots after admitted work and cleanup settle without blocking the next turn. Generated-by: OpenAI Codex
Handle module loading and client construction inside each bridge's failure boundary so registry status reflects failed connections. Generated-by: OpenAI Codex
Reuse pending focus to keep the window hidden during initial loading, allow cancellation, and preserve synchronous warm shortcuts. Generated-by: OpenAI Codex
Initialize composer attachment ownership lazily, avoid empty handshake backing views, clarify line-window and Zod upgrade contracts, and cover CU observer cleanup without restoring allocation probes. Generated-by: OpenAI Codex
English@Astro-Han — follow-up to the summary review. All 11 inline threads have corresponding per-point replies; the ten summary sections are addressed below.
Follow-up changes are pushed through 4bbe8fd. Five affected TypeScript projects compile; 640 tests pass (Graph/Bots 115, WorkHub 24, CU 57, Peer 14, UI 430), plus formatting/whitespace checks. The Graph and SDK regression tests both fail against pre-follow-up source. The full-suite figures already in the PR body are now explicitly labeled as pre-follow-up verification; CI for the new head is running. Automated follow-up by OpenAI Codex. 中文对应总评的逐项跟进。11 个行内 thread 均有对应回复;总评十节处理如下。
后续修复已推送至 4bbe8fd。五个受影响 TypeScript 项目编译通过,640 项测试通过(Graph/Bot 115、WorkHub 24、CU 57、Peer 14、UI 430),格式及空白检查通过。Graph 与 SDK 回归测试在修复前源码上均确认失败。PR 原有全套验证数字现已明确标注为后续修复前的结果,新 HEAD 的 CI 正在运行。 OpenAI Codex 自动跟进回复。 |
Astro-Han
left a comment
There was a problem hiding this comment.
Re-reviewed at b2c6333f4, fresh, against the previous round. Five follow-up commits land the two P1 inline findings and four of the smaller ones, each with a regression test that I confirmed fails on the pre-follow-up source. Of the seven declined items I checked on the code rather than the argument: five rebuttals are correct and two earlier findings were overstated — the replay-identity P1 and the WebRTC half of the shutdown P2 do not describe a present defect, and I withdraw them. One new issue falls out of the cold-summon fix itself and is filed inline.
Status of the previous round
| # | Finding | Status | Where |
|---|---|---|---|
| body 1 | Collapsed reasoning bodies leave the DOM | Declined, partly correct | see P2 below |
| body 2 | Replay identity has two authorities | Declined, rebuttal accepted — withdrawn | verified field-for-field, below |
| body 3a | #retireDriver blocks the next epoch |
Fixed | ab5266ceb, stream-graph-coordinator.ts:1437,1444 |
| body 3b | WebRTC cancel-before-close | Declined, rebuttal accepted — withdrawn | muxer.rs:148 already cancels first |
| body 4 | One invariant rederived 13 times | Declined | acceptable; no defect shown at any site |
| body 5 | base64 validate/measure separable | Declined | all four sites validate first — verified |
| body 6 | structuredClone premise |
Declined with a measurement | acceptable |
| body 7 | Behaviour tests deleted in 44bc559f6 |
Partly addressed | 4 of 8 replaced; see P2 below |
| body 8 | Body entries do not match the diff | Fixed | items 07/27/31/32 corrected; item 07 is waitForConnectionOpen — author is right |
| body 9 | Behaviour fixes filed as allocation work | Declined | commits were already separate; accepted |
| body 10 | Patches have no upstream exit | Partly addressed | zod re-verification note added to patches/README.md:56 |
| inline 1 | require outside the failure boundary |
Fixed | 4aa979987; bot-registry.test.ts:29 is red on base |
| inline 2 | Cold WorkHub summon has no warm-up | Fixed | e2721989f; 2 tests red on base. See P2 below |
| inline 3 | known gate needs a regression test |
Fixed | maka-cu-backend.test.ts:1113; red when the gate is removed |
| inline 4 | WeChat SSE rewrite unmeasured | Declined with a measurement | acceptable |
| inline 5 | Non-lazy useRef lifecycle |
Fixed | use-composer-attachments.ts:214 |
| inline 6 | zod previousHandoff |
Declined, counterexample is valid | the Proxy length getter reenters before alloc |
| inline 7 | chunk clears are no-ops |
Declined with a measurement | correct — block scope says nothing about a suspended frame's registers |
| inline 7b | zero-length handshake remainder | Fixed | peer-native.ts:413 |
| inline 8 | limit === 0 undocumented |
Fixed | text-line-window.ts:22 |
| inline 9 | browser/session.ts comment |
Declined | fine |
| inline 10 | missing views.length === 0 assertion |
Fixed | workhub-presentation.test.ts:559 |
| inline 11 | onClose(); return; |
Declined, rebuttal correct | the early return orphans task's rejection |
On body 2: messageContentsEqual and messageContentDigest do agree on every supported input. AttachmentRef, DirectoryReference, QuoteRef and InlineReference are closed shapes declared through defineObjectShape, and every field of each is compared by the old predicate and kept by normalizeMessageContent; the empty-array-vs-undefined collapse matches on attachments, quotes and directoryReferences, and is deliberately absent on inlineReferences in both. No input separates them, so this is a maintenance preference, not a replay defect. The same applies to the digest change in root-admission-owner.ts.
On body 3b: muxer.rs:148 calls lifetime.cancellation.cancel() before lifetime.close(), so moving the cancel inside close() changes nothing on the production path. The residual delta — workers aborted before peer_connection.close(), where on main there was no cancellation at all — costs at most the tail of an outgoing substream at connection teardown, which resumable-peer-stream retransmits. P3, not blocking.
P2 — Never-expanded reasoning is unreachable to find-in-page and screen readers
Reachability class ① — open a restored transcript and search it.
packages/ui/src/astryx-chat-reasoning.tsx:149 renders {isExpanded || hasExpanded ? children : null}. The previous collapse was grid-template-rows: 0fr plus overflow: hidden, which keeps the subtree in the accessibility tree and findable with Cmd+F. A reasoning block that was never opened in this session — which is every block in a transcript restored from history, exactly where searching matters — now has no text to find.
The process half of the earlier finding does not hold, and I withdraw it: CONTRIBUTING.md:30 reserves the dev@ list for "project direction, governance, and material product decisions" and says implementation-level decisions may live in the pull request. This is an implementation decision.
The accessibility cost stands on its own and is not mitigated by the header staying keyboard-operable — a reader has to know the text is there to expand it. hidden="until-found" restores find-in-page but not the memory, so it is not a free fix. What would settle it is a number: item 22 claims a DOM-residency win with no measurement, and the size of that win is what decides whether the trade is worth making. Either attach one, or scope the deferral to blocks above some length so short reasoning stays searchable.
P2 — Four of the eight behaviour tests are still missing, and they cover the least-verified changes
Reachability class ② — the changes they would cover are cancellation, shutdown and recovery paths.
44bc559f6 remains as pushed. The follow-up added targeted coverage in four existing suites — Graph retirement, BotRegistry SDK failure, WorkHub cold summon, CU session cleanup — and I confirmed all four fail on the pre-follow-up source. The author is right that the file-read and edit-diff files were allocation probes and that some of the others carried GC/WeakRef assertions.
What is left uncovered is the part with the least independent evidence:
5af4f263brestores cancellation through request customization (request-customization-fetch.ts:51). This is a behaviour fix on an abort path with no test that fails without it.1789afeacgates the Goal token cache on the control lease (goal-coordinator.ts:165) and clears it on removal.- The zod patch's
open/handoffhandling, whose only evidence is the counterexample script in the inline thread. lifetime.rscancellation ordering, whose Rust lifecycle test was removed.
Each is one test in an existing suite, in the same shape as the four that landed. Cherry-picking from 44bc559f6^ is cheaper than rewriting, and the GC assertions can be dropped from the restored files.
Verification
Clean npm ci, all affected workspaces built; runtime 98, runtime-host 108, computer-use 117, ui 430, desktop main 2468 green; lint and format clean. Red-on-base (production file reverted alone): graph epoch handover, BotRegistry SDK failure, both WorkHub cold-summon tests, CU known gate — all five fail before the follow-up. Also re-derived with no defect found: the four Buffer.byteLength(x, 'base64') sites, sliceLineByBytes / truncateToolOutput at degenerate budgets, a 120 000-case base-vs-head differential fuzz of createEditUnifiedDiff / createUnifiedDiff (zero diffs), the peer-mesh/node.ts wake rewrite, the app-shell.tsx hydration fence, and the WeChat iterator completion protocol.
Nothing else at P0–P2. With the three above addressed — or, for the first two, answered with a measurement — this is ready to approve.
中文
在 b2c6333f4 上重新评审。五个跟进 commit 落实了两条 P1 行内意见和四条小意见,每条都带回归测试;我逐一在跟进前的源码上确认它们会失败。七条被拒的意见我按代码而不是按论据核对:五条反驳成立,两条原意见站不住——重放身份那条 P1 和关闭顺序 P2 的 WebRTC 一半都不构成现存缺陷,我撤回。冷启动那条修复本身引出一个新问题,已作为行内评论提出。
上一轮意见的处理状态
| # | 意见 | 状态 | 位置 |
|---|---|---|---|
| 正文 1 | 折叠 reasoning 正文离开 DOM | 拒绝,部分成立 | 见下方 P2 |
| 正文 2 | 重放身份有两个权威 | 拒绝,反驳成立——撤回 | 已逐字段核对,见下 |
| 正文 3a | #retireDriver 阻塞下一个 epoch |
已修 | ab5266ceb,stream-graph-coordinator.ts:1437,1444 |
| 正文 3b | WebRTC 先 cancel 后 close | 拒绝,反驳成立——撤回 | muxer.rs:148 本就先 cancel |
| 正文 4 | 同一不变量推导 13 遍 | 拒绝 | 可接受;任何调用点都没证明出缺陷 |
| 正文 5 | base64 校验与测长可分离 | 拒绝 | 四个调用点都先校验——已核实 |
| 正文 6 | structuredClone 前提 |
拒绝并给出测量 | 可接受 |
| 正文 7 | 44bc559f6 删掉行为测试 |
部分处理 | 8 个补回 4 个;见下方 P2 |
| 正文 8 | 正文条目与 diff 不符 | 已修 | 07/27/31/32 已更正;07 确为 waitForConnectionOpen,作者是对的 |
| 正文 9 | 行为修复归在分配优化下 | 拒绝 | commit 本就是分开的;接受 |
| 正文 10 | 补丁没有上游出口 | 部分处理 | patches/README.md:56 补了 zod 升级复核要求 |
| 行内 1 | require 在失败边界外 |
已修 | 4aa979987;bot-registry.test.ts:29 在基线上会红 |
| 行内 2 | 冷启动快捷键没有预热 | 已修 | e2721989f;两个测试在基线上会红。见下方 P2 |
| 行内 3 | known 门需要回归测试 |
已修 | maka-cu-backend.test.ts:1113;抽掉那个门就会红 |
| 行内 4 | 微信 SSE 重写没有测量 | 拒绝并给出测量 | 可接受 |
| 行内 5 | useRef 非惰性 |
已修 | use-composer-attachments.ts:214 |
| 行内 6 | zod previousHandoff |
拒绝,反例成立 | 数组 Proxy 的 length getter 在 alloc 前重入 |
| 行内 7 | chunk 清空是 no-op |
拒绝并给出测量 | 正确——块作用域说明不了挂起帧的寄存器 |
| 行内 7b | 零长度握手 remainder | 已修 | peer-native.ts:413 |
| 行内 8 | limit === 0 未写明 |
已修 | text-line-window.ts:22 |
| 行内 9 | browser/session.ts 注释 |
拒绝 | 可以 |
| 行内 10 | 缺 views.length === 0 断言 |
已修 | workhub-presentation.test.ts:559 |
| 行内 11 | onClose(); return; |
拒绝,反驳正确 | 提前 return 会让 task 的拒绝无人接管 |
关于正文 2:messageContentsEqual 与 messageContentDigest 在所有受支持输入上确实一致。AttachmentRef、DirectoryReference、QuoteRef、InlineReference 都是经 defineObjectShape 声明的封闭形状,每个字段旧判据都比较、normalizeMessageContent 都保留;空数组与 undefined 的归一在 attachments、quotes、directoryReferences 上两边一致,在 inlineReferences 上两边同样刻意不做。没有输入能把两者分开,所以这是维护偏好而不是重放缺陷。root-admission-owner.ts 的摘要改动同理。
关于正文 3b:muxer.rs:148 在 lifetime.close() 之前就调用了 lifetime.cancellation.cancel(),把 cancel 挪到 close() 里面对生产路径毫无影响。残留差异——worker 在 peer_connection.close() 之前被取消,而 main 上根本没有取消机制——最多损失连接拆除时某个子流的尾部字节,resumable-peer-stream 会重传。P3,不阻塞。
P2 — 从未展开的 reasoning 对页内查找和屏幕阅读器不可达
可达类别 ①——打开一份恢复的会话记录并搜索它。
packages/ui/src/astryx-chat-reasoning.tsx:149 渲染 {isExpanded || hasExpanded ? children : null}。原来的折叠是 grid-template-rows: 0fr 加 overflow: hidden,内容留在可访问性树里、能被 Cmd+F 命中。本次会话里从未被打开过的 reasoning 块——也就是从历史恢复的记录里的每一个块,恰恰是最需要搜索的地方——现在没有任何文本可供查找。
原意见中的流程论据不成立,我撤回:CONTRIBUTING.md:30 把 dev@ 邮件列表限定为"项目方向、治理与实质产品决策",并写明实现层面的决策可以留在 PR 里。这属于实现决策。
可访问性代价本身成立,"header 仍可键盘操作"并不能抵消——读者得先知道文本在那里才会去展开。hidden="until-found" 能恢复页内查找但保不住内存,所以不是免费的修法。真正能定案的是一个数字:第 22 项声称的 DOM 驻留收益没有测量,而这个收益的大小决定了这笔交换值不值。要么补上测量,要么把延迟挂载限定在超过某个长度的块上,让短 reasoning 保持可搜索。
P2 — 八个行为测试还缺四个,而且缺的正是独立证据最少的部分
可达类别 ②——它们要覆盖的是取消、关闭与恢复路径。
44bc559f6 保持原样。跟进在四个既有套件里补了针对性覆盖——Graph 退役、BotRegistry SDK 失败、WorkHub 冷启动、CU 会话清理——我确认这四个在跟进前的源码上都会失败。作者说得对:file-read 与 edit-diff 那两个文件是分配量探针,其余几个里确实有 GC/WeakRef 断言。
剩下没覆盖的,正是独立证据最少的部分:
5af4f263b恢复了请求定制路径上的取消(request-customization-fetch.ts:51)。这是 abort 路径上的行为修复,没有任何测试会因为缺它而失败。1789afeac把 Goal token 缓存挂到 control lease 上(goal-coordinator.ts:165),并在移除时清理。- zod 补丁的
open/handoff处理,目前唯一证据是行内回复里的反例脚本。 lifetime.rs的取消顺序,对应的 Rust 生命周期测试已被删除。
每一项都是在既有套件里加一个测试,形状和已落地的那四个一样。从 44bc559f6^ 挑回来比重写便宜,恢复的文件里去掉 GC 断言即可。
验证
干净 npm ci,受影响 workspace 全部构建;runtime 98、runtime-host 108、computer-use 117、ui 430、desktop main 2468 全绿;lint、format 干净。基线红检(只回退生产文件):graph epoch 交接、BotRegistry SDK 失败、两个 WorkHub 冷启动测试、CU known 门——五项在跟进前全部失败。另外独立推导未发现缺陷:四个 Buffer.byteLength(x, 'base64') 调用点、sliceLineByBytes / truncateToolOutput 的退化预算、createEditUnifiedDiff / createUnifiedDiff 基线对 head 的 120 000 组差分 fuzz(零差异)、peer-mesh/node.ts 唤醒重写、app-shell.tsx 水合 fence、微信迭代器完成协议。
P0–P2 没有其他问题。上面三条处理掉——前两条也可以用一次测量来回答——就可以 approve。
| if (!view || view.webContents.isDestroyed() || !rendererReady || !parent || parent.isDestroyed()) return; | ||
| // A cold summon stays hidden until the renderer has mounted its composer. | ||
| // Reuse focusPending so hide/disable can cancel it before ready arrives. | ||
| if (placement === 'floating' && progressRequest === undefined) { |
There was a problem hiding this comment.
A cold summon now keeps the floating window hidden for the whole renderer bootstrap, and ensureView() never clears backgroundThrottling — so the bootstrap this item is trying to speed up runs in a backgrounded, timer-throttled renderer. requestProgress() at :365 already handles the same "paint while hidden, show on acknowledgement" shape with setBackgroundThrottling(false), restored by clearProgressRequest() at :304. Do the same here: disable it when the cold summon starts and re-enable it in the ready branch at :476. (Not a deadlock — ready comes from a plain mount effect, not an animation frame.)
中文
冷启动现在让浮窗在整个渲染器启动期间保持隐藏,而 ensureView() 从不清除 backgroundThrottling——于是本项想加速的那段启动过程跑在一个被后台化、定时器被节流的渲染器里。requestProgress() 在 :365 已经用 setBackgroundThrottling(false) 处理了同样的「隐藏中绘制、收到确认再显示」形状,并由 :304 的 clearProgressRequest() 恢复。这里照做:冷启动开始时关掉,在 :476 的 ready 分支恢复。(不会死锁——ready 来自普通挂载 effect,不经动画帧。)
There was a problem hiding this comment.
English
Keeping the current implementation: the new hidden window is not initially a backgrounded document. Electron's page-visibility contract gives a show:false window initial visible state; this path does not set paintWhenInitiallyHidden:false.
I checked the actual Desktop bootstrap on Linux/xvfb, Electron 43.4.1 / Chromium 150 / Node 24.18.1, with fresh profiles. The final A/B recorded visibility in an init script and at DOMContentLoaded, before first show:
| Observation | Default throttling | Explicitly disabled |
|---|---|---|
| Native window visible at DOMContentLoaded | false | false |
| Initial / DOMContentLoaded document visibility | visible / visible | visible / visible |
| WebContents creation → first show | 337.89 ms | 333.84 ms |
The probe bypassed Playwright's Electron loader, asserted that disable-background-timer-throttling was absent, and disabled focus emulation. This is path verification, not cross-platform latency statistics.
The progress path reuses a previously hidden container and waits for two animation frames; normal cold readiness comes from a mount effect. They do not establish the same background premise. I therefore am not adding a throttle toggle for an unproven cold-start delay. An unconditional reset in normal ready could also interfere with a pending progress preparation.
中文
维持当前实现:新建隐藏窗口不等于文档已进入后台。Electron 的可见性契约规定 show:false 窗口的初始文档状态仍为 visible;此路径没有设置 paintWhenInitiallyHidden:false。
真实 Desktop bootstrap,Linux/xvfb、Electron 43.4.1/Chromium 150/Node 24.18.1、独立 profile:在 init script 和 DOMContentLoaded 中提前记录可见性,两组在首次显示前原生窗口均为不可见,文档初始及 DOMContentLoaded 状态均为 visible。默认节流/主动关闭节流的 WebContents 创建至首次 show 分别为 337.89/333.84 ms。探针绕过 Playwright Electron loader,确认没有 disable-background-timer-throttling,并关闭 focus emulation;这不是跨平台延迟统计。
进度路径复用已隐藏过的容器、等待两次动画帧,普通冷启动 ready 来自 mount effect,不能直接套用相同后台前提。因此不为尚未证实的冷启动延迟新增节流切换;普通 ready 中无条件恢复也可能干扰尚未完成的进度准备。
Generated-by: OpenAI Codex
EnglishFollow-up to the second review, pushed as Coverage: accepted the four narrow contract gaps, without restoring the deleted allocation/stress suites.
Two affected TypeScript workspaces build; 18 targeted JS tests and all 40 Rust tests pass. Lint/format, ASF headers and Rust Clippy pass. The signal/lease/handoff fault-injection failures above were checked independently; no new full Desktop or cross-platform run is claimed. Reasoning: measurement and cost are now in item 22's accompanying PR-body section. Real
The 5.67 MiB difference is component residency, not OS RSS or a typical-workload claim. The fresh page-search probe does find the marker before expansion in the old version, but only after expansion now. My earlier “not found in either version” probe was not reproducible and is not a rebuttal. Keyboard-operable headers do not replace discoverability; real screen readers were not tested. I retain first-expansion mounting with that disclosed trade-off, without inventing a length threshold. WorkHub: answered in the new inline thread with initial visibility and cold-summon A/B evidence; no throttle toggle added. The previously accepted/withdrawn points stay closed to this adjudication. 中文针对第二轮 review的跟进已推送: **测试覆盖:**采纳四个窄契约覆盖点,不恢复已删除的分配量/压力测试套件。
两个受影响 TypeScript 工作区编译通过,18 项定向 JS 测试、Rust 全套 40 项、lint/格式、ASF 文件头及 Rust Clippy 通过。上述三种故障注入已分别确认失败;本轮没有重跑完整 Desktop 或跨平台验证。 **Reasoning:**测量条件与代价已补入 PR 正文第 22 项的配套说明。真实 TurnView、独立 Chromium 151 进程、固定合成样本:20 段 reasoning × 180 行代码、20 个折叠工具结果、一个最终答复,输入 viewmodel JSON 均为 376,472 字节。GC 后 JS heap 从 10,839,440 降至 4,895,284 字节,约少 5.67 MiB;DOM 元素从 5,020 降至 679。这不是 OS RSS 或典型使用分布。 本轮页内查找复验确认:旧版展开前能找到深层标记,新版须先展开。此前“两版都找不到”的探针未能复现,不再用作反驳。键盘可展开不能替代内容可发现性;未测试真实屏幕阅读器。继续维持首次展开挂载,披露这一取舍,不凭空增加长度阈值。 **WorkHub:**已在新增行内线程给出初始可见性及冷召唤 A/B 证据,不新增节流切换。已接受/撤回的旧项不重新开启裁决。 |
Astro-Han
left a comment
There was a problem hiding this comment.
Third round on 2bb009541. Two of the three items are settled; the new Rust test does not pass on macOS.
1. Reasoning DOM residency — accepted. The measurement is there and the method is sound for what it claims: identical 376,472-byte viewmodel, real TurnView, separate renderer processes, post-GC heap and DOM element counts (10,839,440 → 4,895,284 B; 5,020 → 679 elements), explicitly scoped to component residency rather than RSS or a typical workload. Retracting the earlier "not found in either version" probe and stating plainly that find-in-page now requires expansion is the right call, and it makes the trade-off reviewable instead of disputed. hidden="until-found" is correctly rejected: it only works on a mounted subtree, which is exactly the residency this change removes, and the collapsed panel is display-hidden, so screen readers never reached that content in either version either. No length threshold needed.
2. Restored coverage — three of four confirmed, lifetime.rs not. Verified by reverting each production change and rebuilding: request-customization-fetch.ts:51 (both new cases red — the signal assertion, and the HTTP one timing out at ~2.1 s as described), goal-coordinator.ts:165 (baseline 30 instead of 120, exactly as claimed), and the zod handoff restore (both ESM and CJS red; removing the try/finally alone also turns ESM red, so throw-recovery is covered too). connection_teardown_releases_a_backpressured_writer fails on macOS 26.6 arm64, 3/3 runs, at tests.rs:144 — the 4 MiB write_all returns Ok(Ok(())) inside 100 ms, so the precondition that the writer is backpressured never holds and the teardown contract is never exercised. CI only runs cargo test on ubuntu-24.04 (runtime-host-peer-admission.yml:56), so it stays green there while failing for every macOS contributor. Fix: instead of asserting one fixed-size write stalls, write bounded chunks in a loop until a write actually fails to complete within the window (or drive the write in a task and assert only the release), so the test states the contract without depending on a host's SCTP buffer size.
3. WorkHub background throttling — accepted, rejection is correct. Electron's Page visibility section says a window created with show: false gets initial visibility state visible despite being hidden, and this path does not set paintWhenInitiallyHidden: false, so the cold-summon bootstrap is not a backgrounded document. The code confirms the asymmetry with requestProgress independently: the progress card signals readiness from a double requestAnimationFrame (workhub-progress-card.tsx:57), which a suspended RAF would deadlock — that is why #5114 disabled throttling at :365 — whereas cold-summon readiness comes from a mount effect (workhub-surface-switch.tsx:33), which throttling cannot stall. The 337.89 vs 333.84 ms A/B is consistent with that and no toggle is warranted.
中文
针对 2bb009541 的第三轮。三项中两项已了结;新增的 Rust 测试在 macOS 上不通过。
1. Reasoning DOM 驻留 —— 接受。 测量已给出,方法与其声称的范围相符:同一份 376,472 字节 viewmodel、真实 TurnView、独立渲染进程、GC 后堆与 DOM 元素数(10,839,440 → 4,895,284 字节;5,020 → 679 个元素),并明确限定为组件驻留,而非 RSS 或典型负载。撤回此前“两版都找不到”的探针、直说现在页内查找必须先展开,是正确的处理,使这个取舍变成可评审的事实而非争议。hidden="until-found" 被否掉也是对的:它只对已挂载的子树有效,而这正是本次改动要去掉的驻留;且折叠面板本身是隐藏显示的,两个版本里屏幕阅读器都读不到该内容。不需要长度阈值。
2. 恢复的覆盖 —— 四项中确认三项,lifetime.rs 未确认。 逐个回退生产改动并重新构建验证:request-customization-fetch.ts:51(两个新用例均转红——signal 断言,以及 HTTP 用例如其所述在约 2.1 秒超时)、goal-coordinator.ts:165(基线变成 30 而非 120,与其所称完全一致)、zod 的 handoff 恢复(ESM 与 CJS 均转红;单独去掉 try/finally 也会让 ESM 转红,说明抛错恢复同样被覆盖)。connection_teardown_releases_a_backpressured_writer 在 macOS 26.6 arm64 上 3/3 次失败于 tests.rs:144——4 MiB 的 write_all 在 100 ms 内返回 Ok(Ok(())),写入被背压的前提根本不成立,拆除契约也就从未被执行。CI 只在 ubuntu-24.04 上跑 cargo test(runtime-host-peer-admission.yml:56),所以 CI 绿而每个 macOS 贡献者本地都红。修法:不要断言某个固定大小的写入会停住,而是循环写入有界分块直到某次写入确实在窗口内未完成(或把写入放进任务、只断言释放),让测试表达契约而不依赖某台主机的 SCTP 缓冲大小。
3. WorkHub 后台节流 —— 接受,拒绝是正确的。 Electron 的 Page visibility 一节写明,以 show: false 创建的窗口初始可见性状态为 visible,尽管窗口实际隐藏;此路径也没有设置 paintWhenInitiallyHidden: false,因此冷召唤的 bootstrap 不是后台文档。代码本身独立佐证了与 requestProgress 的不对称:进度卡片的就绪信号来自双重 requestAnimationFrame(workhub-progress-card.tsx:57),RAF 被挂起就会死锁——这正是 #5114 在 :365 关闭节流的原因;而冷召唤的就绪来自 mount effect(workhub-surface-switch.tsx:33),节流无法使其停滞。337.89 对 333.84 ms 的 A/B 与此一致,不需要新增开关。
Generated-by: OpenAI Codex
EnglishAccepted the test-portability finding from round three. Fixed in The test now writes and flushes reusable 8 KiB chunks until an individual operation remains pending for 100 ms, instead of requiring a fixed 4 MiB write to stall. Setup has a 5-second deadline and a 64 MiB traffic guard. The timed-out operation is retained—not cancelled or restarted—through explicit close or drop, with the application reader still alive. The pending operation must settle within 5 seconds. Already-queued bytes may complete successfully during close, so the test does not incorrectly require that operation to fail; it separately asserts that a new write/flush after teardown fails. Each chunk is flushed to keep libp2p framing within the transport message limit. Validation on Linux x86_64: the targeted case passed 10 consecutive reruns, the full Rust suite passed 40/40, and Clippy/all-targets plus formatting passed. No production cancellation order or buffer configuration changed. I cannot independently verify macOS here; please rerun this revision on the macOS machine that exposed the failure. 中文采纳第三轮指出的测试可移植性问题,已在 测试改为复用 8 KiB 分块逐次写入并 flush,直到某次操作确实持续待决 100 ms,不再要求固定 4 MiB 写入必然阻塞。准备阶段限制为 5 秒、最多 64 MiB 流量;保留超时的同一个 future 穿过显式 close/drop,应用层 reader 始终存活。 待决操作必须在 5 秒内退出。已经排队的字节可能在关闭期间成功完成,因此不再错误地要求该操作必定失败,而是另外断言关闭后的新 write/flush 必须失败。逐块 flush 也使 libp2p 帧保持在传输消息大小限制内。 Linux x86_64 验证:定向用例连续重跑 10 次通过,Rust 全套 40/40、Clippy/all-targets 和格式检查通过。未修改生产取消顺序或缓冲配置。本机无法独立验证 macOS,请在此前复现失败的 macOS 机器上复跑此版本。 |
Astro-Han
left a comment
There was a problem hiding this comment.
Fourth round on 13f27da75. The one open item is fixed: git diff 2bb009541..13f27da75 --stat is webrtc_direct/tests.rs only, +52/-25, no production change. The test now states the contract instead of a host's buffer size, and it passes here.
| Item (round three) | State |
|---|---|
| 1. Reasoning DOM residency | settled (accepted round three) |
2. Restored coverage — request-customization-fetch, goal-coordinator, zod handoff |
settled (verified round three) |
2. connection_teardown_releases_a_backpressured_writer portability |
fixed in 13f27da75 |
| 3. WorkHub background throttling | settled (accepted round three) |
Backpressure is now observed, not assumed. The loop at native/runtime-host-peer/src/webrtc_direct/tests.rs:146 writes and flushes 8 KiB chunks until one operation is still pending after 100 ms, and carries that future through teardown — it is not a bigger fixed write or a longer sleep. Instrumenting the loop on this machine shows it needs 6,643,712 bytes before a write stays pending, identically for both explicit_close arms, which is exactly why the previous fixed 4 MiB write_all returned Ok inside the window here.
It cannot hang. Both guards are checked before each attempt: sent < 64 MiB (:147) and a 5-second setup_deadline (:151), so a host that never backpressures fails with a named panic rather than spinning. Every await after that is inside a 5-second timeout.
Dropping the pending write's error assertion is the right call. Bytes already queued can legitimately complete during close, so let _ = ...timeout(..., &mut writing) (:180) keeps the part that is actually contractual — the operation must settle, not stay parked — and the observable claim moves to the post-teardown write at :184, which must fail. That is a stronger statement than the old one and does not encode a close/cancel ordering.
Verified on macOS 26.6 arm64 (cargo 1.98.0): cargo test --locked --lib webrtc_direct passed 5/5 consecutive runs (6 tests each), and cargo test --locked for the crate passed 39/39. The probe edit above was reverted before those runs; the tree matches 13f27da75. No P0–P2 findings, and nothing at P3.
Nothing outstanding; approving.
中文
针对 13f27da75 的第四轮。唯一未了结项已修复:git diff 2bb009541..13f27da75 --stat 只动了 webrtc_direct/tests.rs,+52/-25,没有生产改动。测试现在表达的是契约,而不是某台主机的缓冲大小,并且在本机通过。
| 第三轮条目 | 状态 |
|---|---|
| 1. Reasoning DOM 驻留 | 已了结(第三轮接受) |
2. 恢复的覆盖 —— request-customization-fetch、goal-coordinator、zod handoff |
已了结(第三轮已验证) |
2. connection_teardown_releases_a_backpressured_writer 可移植性 |
已在 13f27da75 修复 |
| 3. WorkHub 后台节流 | 已了结(第三轮接受) |
背压现在是被观察到的,不是假定的。 native/runtime-host-peer/src/webrtc_direct/tests.rs:146 的循环逐次写入并 flush 8 KiB 分块,直到某次操作在 100 ms 后仍然待决,并把同一个 future 带过拆除——不是更大的固定写入,也不是更长的 sleep。在本机给循环加探针可见:需要 6,643,712 字节才会出现真正待决的写入,两个 explicit_close 分支完全一致;这正是此前固定 4 MiB 的 write_all 在本机窗口内返回 Ok 的原因。
它不会挂住。 两个保护在每次尝试前都会检查:sent < 64 MiB(:147)和 5 秒的 setup_deadline(:151),因此永不背压的主机会以具名 panic 失败,而不是空转。之后每个 await 都包在 5 秒 timeout 里。
去掉对待决写入的错误断言是对的。 已排队的字节在关闭期间合法地完成是可能的,因此 let _ = ...timeout(..., &mut writing)(:180)保留了真正属于契约的部分——该操作必须退出,而不是一直卡住——可观察的断言则移到 :184 拆除后的写入,那一次必须失败。这比原来的说法更强,也没有锁死 close/cancel 的顺序。
在 macOS 26.6 arm64(cargo 1.98.0)验证:cargo test --locked --lib webrtc_direct 连续 5 次全部通过(每次 6 项),整个 crate 的 cargo test --locked 通过 39/39。上述探针改动已在这些运行前回退,工作树与 13f27da75 一致。没有 P0–P2 问题,P3 也没有。
没有遗留问题;批准。
Resolve the archive E2E race with main's authoritative busy check and synchronize the merged renderer debt count. Generated-by: OpenAI Codex
|
Approved above. CI is still running on |
Revalidated the byte-count-only protocol changes against current main after merging its Skill query epoch bump. Generated-by: OpenAI Codex
Only `apps/desktop/src/main/workhub-presentation.ts` conflicted. This branch routed WorkHub's summon through `focusWindow(target, deps.revealMode)` at the end of `detach()`; main's #5153 made the WorkHub renderer lazy and moved that reveal out of `detach()` into `focusComposer()`, where a cold summon now waits for the renderer to mount its composer before the native window appears. Resolved by keeping main's placement and this branch's authority: the reveal that `focusComposer()` performs is `focusWindow(parent, deps.revealMode)` instead of the inline `isMinimized()/restore()/show()/focus()`, and `detach()` no longer reveals at all. Every remaining reveal in the file goes through the gate — `focusWindow` in `focusComposer()` and `navigateMain()`, `showWindowInactive` for the progress card — and the lazy-creation path adds no ungated `show()`/`focus()`. Under `hidden` the window stays hidden and the `!parent.isVisible()` early return keeps `focusPending` armed, which is what main's cancellation tests already assert. `workhub-presentation.test.ts` auto-merged but needed one semantic fix: the reveal-mode test asserted on the window immediately after `show()`, which no longer reveals anything. It now asserts nothing is revealed before `ready` and the mode-specific reveal after it. Generated-by: Claude Code
English
Summary
Reduce unused component residency, obsolete object retention and redundant allocations across Desktop, CLI, Runtime, Runtime Host and integrations. Preserve active drafts, attachments, complete durable content and running-work ownership.
Memory growth priority
These classifications describe the behavior before the corresponding fixes. “Cumulative” means obsolete objects remain owned as operations repeat, not that every optimization fixes a leak.
The five metadata items are structurally cumulative but generally smaller than retained message bodies, buffers or native resources.
Unresolved plugin module lifetime
The 64 changes below distinguish lifetime fixes from temporary-allocation reductions. Figures apply only to the specified inputs and paths; they are not additive whole-application or RSS savings.
Item 22 measurement and trade-off
A fixed synthetic transcript rendered through the real
TurnView(20 reasoning blocks × 180 code lines, 20 collapsed tool results, one final answer; identical 376,472-byte viewmodel JSON) was compared in separate Chromium 151 processes. Post-GC JS heap: 10,839,440 → 4,895,284 bytes (5.67 MiB less); DOM elements: 5,020 → 679. This measures component residency, not OS RSS or a representative workload distribution. Text remains retained. Browser page search found a deep marker before expansion in the old implementation but requires first expansion now; opening the first block restored 217 body descendants and marker search. Keyboard expansion does not remove this discovery cost. No real screen-reader test was performed. Keep first-expansion mounting; no unmeasured length threshold is introduced.Verification
Round-two follow-up
2bb009541: added targeted cancellation, Goal lease/baseline, Zod ESM/CJS recursive identity, and WebRTC teardown coverage. Two affected TypeScript workspaces build; 18 targeted JS tests and all 40 Rust tests pass; lint/format, ASF headers and Rust Clippy pass. Fault-injection checks fail when request signal forwarding, the Goal lease gate, or Zod handoff restoration is removed. These are contract tests, not allocation-size benchmarks; no production implementation changed in this follow-up.Review follow-up
4bbe8fd3d: five affected TypeScript projects compile; 640 tests pass (Graph/Bots 115, WorkHub 24, Computer Use 57, Peer 14, UI 430). The Graph and SDK regressions also fail against pre-follow-up source. No new allocation-probe files were added.Original 64-change verification, before these follow-ups:
UI command output:
Not covered: full Windows/macOS packaged-app suites, live Feishu/WeCom connections or all-platform native scenarios. Allocation-specific regression tests are not included in this PR.
AI use
Tool(s) and scope: OpenAI Codex — implementation, verification and PR description. Affected commits include
Generated-by: OpenAI Codex.Checklist
Does this PR entail a change in behavior?
中文
概要
减少 Desktop、CLI、Runtime、Runtime Host 与集成模块中未使用组件的驻留、过期对象保留及重复分配。保留活动草稿、附件、完整持久化内容及运行中任务的所有权。
内存增长优先级
以下分类描述各项修复前的行为。“持续累积”指旧对象随重复操作继续被持有,不代表每项优化都是泄漏修复。
五项元数据问题虽会持续累积,但通常不能与完整消息、Buffer 或原生资源按同等量级看待。
尚未解决的插件模块生命周期
以下 64 项区分生命周期修复与临时分配优化。数值仅适用于注明的输入及路径,不能相加作为整应用或 RSS 节省量。
第 22 项测量与取舍
用真实
TurnView渲染固定合成会话:20 段 reasoning,每段 180 行代码,20 个折叠工具结果及一个最终答复;两版输入 viewmodel JSON 均为 376,472 字节。在独立 Chromium 151 进程中,GC 后 JS heap 为 10,839,440 → 4,895,284 字节(减少约 5.67 MiB),DOM 元素为 5,020 → 679。这是组件驻留测量,不是 OS RSS 或典型用户分布;原文仍保留。旧实现展开前能通过浏览器页内查找命中深层标记,新实现须先展开;展开第一段后恢复 217 个正文后代元素及标记查找。键盘可展开不能抵消这一发现成本。未测试真实屏幕阅读器。维持首次展开挂载,不新增缺乏测量依据的长度阈值。验证
第二轮跟进
2bb009541:补充请求取消、Goal lease/token 基线、Zod ESM/CJS 循环身份和 WebRTC 拆除行为覆盖。两个受影响 TypeScript 工作区编译通过;18 项定向 JS 测试、Rust 全套 40 项、lint/格式、ASF 文件头及 Rust Clippy 通过。分别移除请求 signal 转发、Goal lease 检查或 Zod handoff 恢复时,反向测试均失败。新增的是契约测试,不是分配量基准;本轮未改生产实现。Review 后续修复
4bbe8fd3d:五个受影响 TypeScript 项目编译通过;640 项测试通过(Graph/Bot 115、WorkHub 24、Computer Use 57、Peer 14、UI 430)。Graph 与 SDK 回归测试在修复前源码上也确认失败。未新增分配量探针文件。以下为后续修复之前的原 64 项变更验证:
界面验证命令输出:
未覆盖:Windows/macOS 打包应用全套、真实飞书/企业微信连接及全部平台原生场景。本 PR 不包含分配量专项回归测试。
AI 使用声明
工具与范围:OpenAI Codex — 实现、验证及 PR 说明。相关提交包含
Generated-by: OpenAI Codex。检查清单
本 PR 是否改变行为?